#! user/bin/python
# -*- coding: utf-8 -*-

"""
Created on 2021/8/27 15:46

@File: mailSender.py
@Author: LT
@Description: 其实这个模块如果没有：
              fix_encode_base64和fix_text_required的话，则好理解很多，就是封装啊封装
"""
from .. import 邮件配置模块_mailconfig                # 客户端的mailconfig
import smtplib, os, mimetypes                       # mime: 文件名到类型
import email.utils, email.encoders                  # 日期字符串，base64格式
from .mailTool类 import MailTool, SilentMailTool

from email.message import Message                   # 普通消息，对象->文本
from email.mime.multipart import MIMEMultipart      # 类型特异的消息
from email.mime.audio import MIMEAudio              # 格式化/编码附件
from email.mime.image import MIMEImage
from email.mime.text import MIMEText
from email.mime.base import MIMEBase
from email.mime.application import MIMEApplication  # 使用新app类


def fix_encode_base64(msgobj):
    """
    书第四版：对于Python 3.1 email包里不能从base64或其他email编码格式编码的二进制部分，生成邮件文本漏洞的规避措施；
    构造器运行的正常的email.encoder即便在负载编码为base64文本后，仍将其保留为bytes类型；这样打断了邮件文本的生成，后者
    """

    linelen = 76                                    # 按照 MIME 标准的规定
    from email.encoders import encode_base64

    encode_base64(msgobj)                           # email的通常做法：保留为bytes类型
    text = msgobj.get_payload()                     # bytes在email包进行文本生成时导致失败
    if isinstance(text, bytes):                     # 负载在3.1中为bytes类型，3.2及之后版本为str类型
        text = text.decode('ascii')                 # 解码成unicode str 以便进行文本生成

    lines = []                                      # 分割成多行，否则会是很长的一行
    text = text.replace('\n', '')                   # 3.1中没有在其中出现\n，不过仍这样做会在之后的版本中证明是正确的处理
    while text:
        line, text = text[:linelen], text[linelen:]
        lines.append(line)
    msgobj.set_payload('\n'.join(lines))


def fix_text_required(encodingname):
    """
    书第四版：对于Python 3.1 email包里的str/bytes组合错误的一种规避措施；
    :return -> boolean
    """
    from email.charset import Charset, BASE64, QP

    charset = Charset(encodingname)         # how email knows what to do for encoding
    bodyenc = charset.body_encoding         # utf8, 其他编码要求bytes类型输入数据
    return bodyenc in (None, QP)            # ascii, latin1, 其他编码要求str


class MailSender(MailTool):
    """
    发送邮件：格式化一条消息，与SMTP服务器建立接口；可在任何带有Python的联网的机子上运行，不再需要使用cmdline了；
    不要求验证客户端，若要求登录验证，则移步MailSenderAuth；
    tracesize代表被追踪的消息文本的数字字符串：0=没有，big=所有；
    支持文本主体和文本部分的Unicode编码；
    支持题头的编码，包括完整题头和电子邮件姓名；
    """

    def __init__(self, smtpserver=None, tracesize=256):
        self.smtpServerName = smtpserver or 邮件配置模块_mailconfig.smtpservername
        self.tracesize = tracesize

    def sendMessage(self, From, To, Subj, extrahdrs, bodytext, attaches,
                    saveMailSeparator=(('=' * 80) + 'PY\n'),
                    bodytextEncoding='us-ascii',
                    attachesEncodings=None):
        """
        格式化并发送邮件：阻塞调用函数，在GUI里把这个函数作为线程执行（翻译应该是这个意思）；
        bodytext是主体文本部分，attaches是附件文件名组成的列表，extrahdrs是(姓名,邮箱地址)元组构成的列表；
        """

        # 假定主体文本已经编码为目标形式；
        # 客户端可以将其解码为用户选定的，默认的，或utf8备用编码形式
        # 不论那种操作，email需要相应的str异或bytes类型

        if fix_text_required(bodytextEncoding):
            if not isinstance(bodytext, str):
                bodytext = bodytext.decode(bodytextEncoding)
        else:
            if not isinstance(bodytext, bytes):
                bodytext = bodytext.encode(bodytextEncoding)

        # 创建根message
        if not attaches:
            msg = Message()
            msg.set_payload(bodytext, charset=bodytextEncoding)
        else:
            msg = MIMEMultipart()
            self.addAttachments(msg, bodytext, attaches,
                                bodytextEncoding, attachesEncodings)

        # 非ASCII题头在发送时进行编码；只编码地址里的名字，否则smtp彻底会完全放弃信息；
        # 编码所有信封，包括收件姓名（而非地址），并假定服务器允许此操作；
        # msg.as_string会保留题头编码所添加的任何换行符；

        hdrenc = 邮件配置模块_mailconfig.headersEncodeTo or 'utf-8'       # default=utf8
        Subj = self.encodeHeader(Subj, hdrenc)                          # 完整题头，主题
        From = self.encodeAddrHeader(From, hdrenc)                      # 发件人名字
        To = [self.encodeAddrHeader(T, hdrenc) for T in To]             # 各个收件人
        Tos = ', '.join(To)                                             # 题头+信封

        # 添加以上信息到根message中
        msg['From'] = From
        msg['To'] = Tos                                                 # 可能有多个地址
        msg['Subject'] = Subj                                           # 服务器端拒绝以';'作为分隔符
        msg['Date'] = email.utils.formatdate()                          # curr datetime, rfc2822 utc
        recip = To
        for name, value in extrahdrs:                                   # 抄送, 密送, X-Mailer, 等等.
            if value:
                if name.lower() not in ['cc', 'bcc']:
                    value = self.encodeHeader(value, hdrenc)
                    msg[name] = value
                else:
                    value = [self.encodeAddrHeader(V, hdrenc) for V in value]
                    recip += value                                      # some servers reject ['']
                    if name.lower() != 'bcc':                           # 密送获取邮件，不带题头
                        msg[name] = ', '.join(value)                    # 在各抄送间添加逗号

        recip = list(set(recip))                                        # 移除重复项
        fullText = msg.as_string()                                      # 生成标准格式化后的信息

        # 如果所有的Tos发送都失败了则抛出异常
        # 或者返回含有所有失败发送的Tos字典

        self.trace('Sending to...' + str(recip))
        self.trace(fullText[:self.tracesize])                           # SMTP调用进行连接
        server = smtplib.SMTP(self.smtpServerName, timeout=20)          # 这个操作可能会失败
        print("hi wi am stucked.")
        self.getPassword()
        self.authenticateServer(server)                                 # 在子类中登录

        try:
            failed = server.sendmail(From, recip, fullText)             # 正常执行则返回字典
        except:
            server.close()                                              # quit可能会导致挂起
            raise                                                       # 再次抛出异常
        else:
            server.quit()                                               # 连接和发送均OK的情况下quit
        self.saveSentMessage(fullText, saveMailSeparator)               # 首先进行此操作
        if failed:
            class SomeAddrsFailed(Exception): pass

            raise SomeAddrsFailed('Failed addrs:%s\n' % failed)
        self.trace('Send exit')

    def addAttachments(self, mainmsg, bodytext, attaches,
                       bodytextEncoding, attachesEncodings):
        """
        格式化一条带有附件的多部分组成的消息；如果传入文本部分，则对其使用Unicode编码
        """
        # 添加主体文本/纯文本部分
        msg = MIMEText(bodytext, _charset=bodytextEncoding)
        mainmsg.attach(msg)

        # 添加附件部分
        encodings = attachesEncodings or (['us-ascii'] * len(attaches))
        for (filename, fileencode) in zip(attaches, encodings):
            # 文件名可能时绝对路径或是相对路径
            if not os.path.isfile(filename):                            # 跳过同名的非文件，也就是目录, etc.
                continue

            # 根据文件后缀名推测内容类型，忽略编码
            contype, encoding = mimetypes.guess_type(filename)
            if contype is None or encoding is not None:                 # 不做推测，是否需要压缩文件?
                contype = 'application/octet-stream'                    # 使用通用默认值
            self.trace('Adding ' + contype)

            # 组件合适类型的Message子类
            maintype, subtype = contype.split('/', 1)
            if maintype == 'text':                                      # 文本需要编码
                if fix_text_required(fileencode):                       # 需要str或bytes
                    data = open(filename, 'r', encoding=fileencode)
                else:
                    data = open(filename, 'rb')
                msg = MIMEText(data.read(), _subtype=subtype, _charset=fileencode)
                data.close()

            elif maintype == 'image':
                data = open(filename, 'rb')                             # 使用二进制
                msg = MIMEImage(
                    data.read(), _subtype=subtype, _encoder=fix_encode_base64)
                data.close()

            elif maintype == 'audio':
                data = open(filename, 'rb')
                msg = MIMEAudio(
                    data.read(), _subtype=subtype, _encoder=fix_encode_base64)
                data.close()

            elif maintype == 'application':                             # 书第四版新特性
                data = open(filename, 'rb')
                msg = MIMEApplication(
                    data.read(), _subtype=subtype, _encoder=fix_encode_base64)
                data.close()

            else:
                data = open(filename, 'rb')                             # application/* 也可以使用二进制打开
                msg = MIMEBase(maintype, subtype)
                msg.set_payload(data.read())
                data.close()                                            # 创建通用类型
                fix_encode_base64(msg)
            # email.encoders.encode_base64(msg)        # 使用base64编码

            # 设置文件名 (ascii or utf8/mime encoded) 并添加到容器
            basename = self.encodeHeader(os.path.basename(filename))
            msg.add_header('Content-Disposition',
                           'attachment', filename=basename)
            mainmsg.attach(msg)

        # mime结构之外的文本，非MIME邮件阅读器可阅读的部分
        mainmsg.preamble = 'A multi-part MIME format message.\n'        # 给根message设置前言属性和值
        mainmsg.epilogue = ''                                           # 给根message设置后记属性和值，确保消息末尾带有换行符

    def saveSentMessage(self, fullText, saveMailSeparator):
        """
        如果任何消息发送成功，将其追加到本地文件末尾；
        客户端：传入你的应用程序使用的分隔符，做分割；
        缺陷：同时用户可能正在这个文件（可能性不大）
        """
        try:
            sentfile = open(邮件配置模块_mailconfig.sentmailfile, 'a',
                            encoding=邮件配置模块_mailconfig.fetchEncoding)
            if fullText[-1] != '\n':                                    # 确保消息末尾带有换行符
                fullText += '\n'
            sentfile.write(saveMailSeparator)
            sentfile.write(fullText)
            sentfile.close()
        except:
            self.trace('Could not save sent message')                   # 不至于崩溃

    def encodeHeader(self, headertext, unicodeencoding='utf-8'):
        """
        按照可选的用户设置或UTF-8，参照email和Unicode标准编码撰写好的非ASCII消息题头内容；
        如果有必要，header.encode在题头字符串中自动添加换行符；
        """
        try:
            headertext.encode('ascii')
        except:
            try:
                hdrobj = email.header.make_header([(headertext, unicodeencoding)])
                headertext = hdrobj.encode()
            except:
                pass                                                    # 如果有必曰，自动分割成多行内容
        return headertext                                               # 如果没有编码成ascii，可能引起smtplib操作失败

    def encodeAddrHeader(self, headertext, unicodeencoding='utf-8'):
        """
        依照电子邮件，MIME和Unicode标准尝试编码电子邮件地址中的非ASCII名字；
        如果失败，则省略名称而只使用地址部分；如果连地址都无法获取，尝试作为整体一起
        解码，否则smtplib在尝试将整个邮件作为ASCII而编码时可能发生错误；
        utf-8格式化代码适用范围较广，应该能胜任多数情况；

        如果过长则插入换行符，hdr.encode分割名字时也将得到多行结果，不过行的长度大于临界值的时候，
        可能会捕获不到（这还有待改进）；
        这里在使用Message.as_string来标准格式化代码时不做进一步拆行；
        mailParser模块里的decodeAddrHeader是本函数的逆向操作；
        """
        try:
            pairs = email.utils.getaddresses([headertext])              # 分割 地址+邮件部分
            encoded = []
            for name, addr in pairs:
                try:
                    name.encode('ascii')                                # 如果ascii编码可行则不加更改地使用
                except UnicodeError:                                    # 否则尝试编码姓名部分
                    try:
                        uni = name.encode(unicodeencoding)
                        hdr = email.header.make_header([(uni, unicodeencoding)])
                        name = hdr.encode()
                    except:
                        name = None                                     # 省掉名字，只使用地址部分
                joined = email.utils.formataddr((name, addr))           # 如有必要，则引用名字
                encoded.append(joined)

            fullhdr = ', '.join(encoded)
            if len(fullhdr) > 72 or '\n' in fullhdr:                    # 如果不是一行短字符串
                fullhdr = ',\n '.join(encoded)                          # 尝试多行
            return fullhdr
        except:
            return self.encodeHeader(headertext)

    def authenticateServer(self, server):
        pass                                                            # 这个服务器/类不要求登录

    def getPassword(self):
        pass                                                            # 这个服务器/类不要求登录


################################################################################
# 专用的子类们
################################################################################

class MailSenderAuth(MailSender):
    """
    这个类用于要求登录验证的子类；
    基于mailconfig.smtpuser的设置
    """
    smtpPassword = None                                                 # 作为一个类，而非放在self中，可能被多个实例共享

    def __init__(self, smtpserver=None, smtpuser=None):
        MailSender.__init__(self, smtpserver)
        self.smtpUser = smtpuser or 邮件配置模块_mailconfig.smtpuser
        # self.smtpPassword = None                                      # 每次发送都让PyMailGUI做请求

    def authenticateServer(self, server):
        server.login(self.smtpUser, self.smtpPassword)

    def getPassword(self):
        """
        如果还没有SMTP验证码则获取之；
        可能由超类自动调用或客户端手动调用：在发送前不需要，不过不在GUI线程里运行；
        从客户端文件或子类方法获取
        """
        if not self.smtpPassword:
            try:
                localfile = open(邮件配置模块_mailconfig.smtppasswdfile)
                MailSenderAuth.smtpPassword = localfile.readline()[:-1]
                self.trace('local file password' + repr(self.smtpPassword))
            except:
                MailSenderAuth.smtpPassword = self.askSmtpPassword()

    def askSmtpPassword(self):
        assert False, 'Subclass must define method'


class MailSenderAuthConsole(MailSenderAuth):
    def askSmtpPassword(self):
        import getpass
        prompt = 'Password for %s on %s?' % (self.smtpUser, self.smtpServerName)
        return getpass.getpass(prompt)                                      # 用了getpass就得用python shell来运行了


class SilentMailSender(SilentMailTool, MailSender):
    pass                                                                 # 需重写trace
